08 Django 会话技术

理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的请求操作则应该属于另一个会话,二者不能混淆。Web 应用程序是使用 Http 协议传输数据的,Http 协议是无状态的协议,一旦数据交换完成,客户端与服务器端的连接就会关闭,再次交换数据需要重新建立连接。这意味着服务器无法从连接上跟踪会话。

要跟踪会话,必须引入一种机制。Cookie 就是这样一种机制。它可以弥补 Http 协议无状态的不足。在 Session 出现之前,基本上所有的网站都采用 Cookie 来跟踪会话。

Cookie 实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器颁发一个 Cookie。客户端浏览器会把 Cookie 保存起来。当浏览器再次请求该网站时,浏览器把请求的网址连同该 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。服务器还可以根据需要修改 Cookie 的内容。

由于 Http 是一种无状态的协议 ,服务器单从网络连接上不能知道客户端身份,怎么办?就给客户端颁发一个通行证,每人一个,无论谁访问都必须携带自己的通行证。这样服务器就能从通行证上确认客户身份了。这就是 Cookie 的工作原理。

Cookie 本身由服务器生成,通过 responseCookie 写到浏览器上,下一次访问,浏览器会根据不同的规则携带 Cookie 过来。

3.png|图 8-1: Cookie 工作原理

注意Cookie 不能跨浏览器,一般不跨域。

使用 response 设置

response.set_cookie(key, value[, max_age=None, expires=None])

max_age:指定 Cookie 过期时间,整数,单位为秒。默认值为 None,浏览器关闭失效

expires:指定过期时间,还支持 datetimetimedelta,可以指定一个具体时间日期。

expires=datetime.datetime(2030, 1, 1, 2, 3, 4)

d = datetime.datetime.now() + datetime.timedelta(days=10)

expires=d

注意max_ageexpires 两个选一个指定

response.set_cookie('username', username, max_age=10)

response.set_cookie('username', username, expires=d)

使用 request 获取

request.COOKIES.get('username')

使用 response 删除

response.delete_cookie('username')

优点:减轻服务器端的压力,提高网站的性能

缺点:安全性不高。在客户端很容易被查看或破解用户会话信息

5. CSRF

CSRFCross Site Request Forgery,跨站请求伪造。

CSRF 是指攻击者盗用了你的身份,以你的名义发送恶意请求。包括以你的名义发送邮件、消息,盗取账号,甚至于购买商品,虚拟货转账等等。

造成个人隐私泄露以及财产安全等问题。

4.png|图 8-2 CSRF 过程

防止 CSRF

Django 下的 CSRF 预防机制。Django 第一次响应来自某个客户端的请求时,会在服务器端随机生成一个 token,把这个 token 放在 Cookie 里,然后每次 POST 请求都会带上这个 token,这样就能避免被 CSRF 攻击。

POST 请求时,表单中添加 {% csrf_token %}

<form action="" method="POST">
	{% csrf_token %}
	...
</form>

6. 模拟用户登录

用户注册、登录、退出登录三个功能

(1)准备

新键 User 应用,并在 templates 目录中新建 index.htmlregister.htmllogin.html 三个文件,先对三个文件内容进行简单标识,如 <title>首页</title>

新建用户模型。在 /User/models.py 中新 UserModel 模型(用户模型),代码如下:

from django.db import models

# 用户模型
class UserModel(models.Model):
	username = models.CharField(max_length=30, unique=True)
	password = models.CharField(max_length=100)
	age = models.IntegerField(default=18)

不用添加用户数据,可以从页面注册。

新键视图函数并配置路由。在 /User/views.py 中新建三个视图函数,实现首页、登录、注册三个页面的跳转。

首页def index(request) 函数

from django.shortcuts import render, HttpResponse, redirect, reverse


def index(request):
	return render(request, "index.html")

注册def register(request) 函数

def register(request):
	return render(request, "register.html")

登录def login(request) 函数

def login(request):
	return render(request, "login.html")

路由。 为了方便使用主路由,但需要指定路由名称,后边会反向解析。

# 项目同名目录下的 urls.py
from User.views import *

urlpatterns = {
	# 首页	
	path("index/", index, name="index")	 # 127.0.0.1:8000/index/ 
	path("", index)   # 也是首页链接,实现的是域名直接显示首页,127.0.0.1:8000/ 即可显示首页

	# 注册
	path("register/", register, name="register")

	# 登录
	path("login/", login, name="login")

	path("admin/", admin.site.urls)
}

(2)注册功能

修改 register.html 内容,添加注册表单信息。

<form action="" method="post">  {# 使用 POST 方法提表单,增强安全性 #}
	{% csrf_token %}   {# CSRF #}
	<p>
		<label>用户名:<input type="text" name="uname"></label>
	</p>
	<p>
		<label>密码:<input type="password" name="passwd"></label>
	</p>
	<p>
		<label>年龄:<input type="tepy" name="age"></label>
	</p>
	<p>
		<button>注册</button>
	</p>
</form>

修改 register 视图函数

def register(request):
	if request.method == "GET":
		return render(request, "register.html")
	elif request.method == "POST":
		# 接收前端提交的数据
		uname = request.POST.get("uname")
		passwd = request.POST.get("passwd")
		age = request.POST.get("age")

		# 先判断用户是否已经被注册过
		users = UserModel.obejcts.filter(username=uname) # 这里使用的是 filter,结果是个集合,因为定义模型时,username 加了 unique 约束,如果用户已经存在,这个结果也是个集合,只不过只有一个元素。如果用户不存在,结果集是个空值。
		if users.exists():
			return HttpResponse("用户已经存在")


		try:
			# 实现注册功能,添加用户数据
			# 上面已经判断过用户名是否存在,这里可以不用 try 
			user = UserModel()
			user.username = uname
			user.password = passwd
			user.age = age
			user.save()
		except:
			return HttpResponse(注册失败)

		# 注册完成跳转到登录页面
		return redirect(reverse("login"))

(3)登录功能

修改 login.html 内容,添加登录表单

<form action="" method="post">
	{% csrf_token %}
	<p>
		<label>用户名:<input type="text" name="uname"></label>
	</p>
	<p>
		<label>密码:<input type="password" name="passwd"></label>
	</p>
	<p>
		<button>登录</button>
	</p>
</form>

修改 login 视图函数

def login(request):
	if request.method == "GET":
		return render(request, "login.html")
	elif request.method == "POST":
		# 1. 接收前端提交的数据
		uname = request.POST.get("uname")
		passwd = request.POST.get("passwd")

		# 2. 登录验证
		users = UserModel.objects.filter(username=uname, password=passwd)
		if user.exists():
			# 获取当前登录的用户对象
			user = users.first()  # 就像之前说的一样,username 添加了 unique 约束,如果 filter 后找到用户,结果集中也只会有一个用户对象,所以直接用 first 取出即可
			# 3. 设置 cookie
			response = redirect(reverse("index"))
			d = datatime.datetime.now() + datetime.timedelta(7)
			response.set_cookie("userid", user.id, expires=d)

			return response

还需要修改 index 视图函数和 index.html 内容。登录后和登录前首面内容显示不同,登录前还是登录后,使用 cookie 判断

修改 index 视图函数

def index(request):
	# 使用 Cookie 判断登录
	userid = reqeust.COOKIES.get("userid", 0)  # COOKIES.get() 第一个参数是,当前网站给这个浏览器设置的 cookie 的名,第二参数是如果没有找到 cookie 名时的默认值
	# 获取登录用户
	user = UserModel.objects.filter(id=userid).first()
	data = {
		"user": user,
	}

	return render(request, "index.html", data)

修改 index.html

{% if user %}
	当前登录的用户: {{ user.username }}
{% else %}
	<a href="{% url 'login' %}">登录</a>
	<a href="{% url 'register' %}">注册</a>
{% endif %}

(4)实现退出登录

退出登录,只是删除 cookie 并跳转到 index 页面,没有对应的 html 文件,所以只需要添加 logout 视图函数、配置路由,即可实现。

# 项目同名目录下的 urls.py 中 urlpatterns 添加 path
path("logout/", logout, name="logout"),

# User/views.py 添加 logout 视图函数
def logout(request):
	# 先获取当前登录的所有响应信息
	response = redirect(reverse("index"))

	# 删除 cookie 即是退出登录
	response.delete_cookie("userid")
	return response

断续修改 index.html,把退出登录链接添加到登录后的页面。

{% if user %}
	当前登录的用户: {{ user.username }} <br>
	<a href="{% url "logout" %}">退出</a>
{% else %}
	<a href="{% url 'login' %}">登录</a>
	<a href="{% url 'register' %}">注册</a>
{% endif %}

二、Session

Session 服务器端会话技术,依赖于 cookie

Django 中户用 Session

setting.py 文件中,INSTALLED_APPS 中添加 django.contrib.sessions,MIDDLEWARE 中添加 django.contrib.session.middleware.SessionMidleware,默认已经配置好了。

1. 设置 Sessions

使用 request 设置

request.session["user_id"] = user.id
request.session.set_expiry(86400)  # 设置过期时间

2. 获取 Sessions

使用 request 设置

# get(key, defautl=None)  # 根据键获取值
session_value = request.session.get("user_id")
# 或
session_value = request.session["user_id"]

3. 删除 Session

# 获取当前登录请求的 session 的 key
session_key = request.session.session_key
# 删除
del request.session[session_key]
# request.session.delete(session_key)

# 删除当前会话数据并删除会话的 cookie 
flush() 

数据存储到数据库中会进行编码,使用的是 Base64

每个 HttpRequest 对象都有一个 Session 属性,也是一个类字典对象。